﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;

namespace Asdo.Controls
{
    /// <summary>
    /// Stack panel with swiping and shifting also support drag-n-drop
    /// </summary>
    public class SwipePanel : Panel
    {
        static SwipePanel()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof (SwipePanel),
                                                     new FrameworkPropertyMetadata(typeof (SwipePanel)));

            #region Orientation
            
            OrientationProperty = DependencyProperty.Register("Orientation", typeof(Orientation), typeof(SwipePanel),
                                                      new FrameworkPropertyMetadata(Orientation.Vertical,
                                                                                    FrameworkPropertyMetadataOptions
                                                                                        .AffectsArrange |
                                                                                    FrameworkPropertyMetadataOptions
                                                                                        .AffectsMeasure |
                                                                                    FrameworkPropertyMetadataOptions
                                                                                        .AffectsRender)); 
            #endregion

            #region Swipe Point
            
            UseSwipePiontProperty = DependencyProperty.Register("UseSwipePiont", typeof(bool), typeof(SwipePanel),
                                                        new FrameworkPropertyMetadata(false));
            IsSwipePointProperty = DependencyProperty.RegisterAttached("IsSwipePoint", typeof(bool),
                                                                       typeof(SwipePanel),
                                                                       new FrameworkPropertyMetadata(false)); 
            #endregion

            #region Events

            MovingEvent = EventManager.RegisterRoutedEvent("Moving", RoutingStrategy.Tunnel, typeof (MovingEventHandler),
                                                           typeof (SwipePanel));
            DragingEvent = EventManager.RegisterRoutedEvent("Draging", RoutingStrategy.Tunnel,
                                                            typeof (MovingEventHandler),
                                                            typeof (SwipePanel));

            #endregion
        }

        #region IsSwipePoint
        
        /// <summary>
        /// Attached property. It use to mark that UIElement is the swipe point
        /// </summary>
        public static readonly DependencyProperty IsSwipePointProperty;

        public static bool GetIsSwipePoint(DependencyObject obj)
        {
            return (bool) obj.GetValue(IsSwipePointProperty);
        }

        public static void SetIsSwipePoint(DependencyObject obj, bool value)
        {
            obj.SetValue(IsSwipePointProperty, value);
        }

        #endregion

        #region UseSwipePiont

        public static readonly DependencyProperty UseSwipePiontProperty;
        
        /// <summary>
        /// It switch on swiping by special points
        /// </summary>
        public bool UseSwipePiont
        {
            get { return (bool) GetValue(UseSwipePiontProperty); }
            set { SetValue(UseSwipePiontProperty, value); }
        }

        #endregion

        #region Orientation

        public static readonly DependencyProperty OrientationProperty;

        /// <summary>
        /// Orientation of items (it works like the stack panel)
        /// </summary>
        public Orientation Orientation
        {
            get { return (Orientation) GetValue(OrientationProperty); }
            set { SetValue(OrientationProperty, value); }
        }

        #endregion

        #region IsSwipeLocked - Attached property - lock swiping

        /// <summary>
        /// Attached property. It helps to lock swiping when item droped on panel.
        /// </summary>
        public static readonly DependencyProperty IsSwipeLockedProperty =
            DependencyProperty.RegisterAttached("IsSwipeLocked", typeof (bool), typeof (SwipePanel),
                                                new FrameworkPropertyMetadata(false));

        public static bool GetIsSwipeLocked(DependencyObject obj)
        {
            return (bool) obj.GetValue(IsSwipeLockedProperty);
        }

        public static void SetIsSwipeLocked(DependencyObject obj, bool value)
        {
            obj.SetValue(IsSwipeLockedProperty, value);
        }

        #endregion

        #region Events

        public static readonly RoutedEvent MovingEvent;
        public static readonly RoutedEvent DragingEvent;

        /// <summary>
        /// It support action after swiping(moving) items.
        /// Handler should move item in self collection.
        /// </summary>
        public event MovingEventHandler Moving
        {
            add { AddHandler(MovingEvent, value); }
            remove { RemoveHandler(MovingEvent, value); }
        }

        /// <summary>
        /// It support action before drag-n-drop items.
        /// Handler should start drag-n-drop procedure.
        /// </summary>
        public event MovingEventHandler Draging
        {
            add { AddHandler(DragingEvent, value); }
            remove { RemoveHandler(DragingEvent, value); }
        }

        #endregion

        #region Size by Orientation

        private double GetSize(Size size, bool invert = false)
        {
            return (Orientation == Orientation.Horizontal) ^ invert ? size.Width : size.Height;
        }

        private void SetSize(Size size, double value, bool invert = false)
        {
            if ((Orientation == Orientation.Horizontal) ^ invert)
            {
                size.Width = value;
            }
            else
            {
                size.Height = value;
            }
        }

        #endregion

        #region Layout

        protected override Size MeasureOverride(Size availableSize)
        {
            SetSize(availableSize, Double.PositiveInfinity);

            double size1 = 0;
            double size2 = 0;

            foreach (UIElement child in Children)
            {
                if (child != null)
                {
                    child.Measure(availableSize);

                    double s = GetSize(child.DesiredSize, true);

                    if (size1 < s)
                    {
                        size1 = s;
                    }

                    size2 += GetSize(child.DesiredSize);
                }
            }

            return Orientation == Orientation.Horizontal ? new Size(size2, size1) : new Size(size1, size2);
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            double offset = 0.0;

            Debug.WriteLine("Arrage override");

            for (int index = 0; index < Children.Count; index++)
            {
                UIElement child = Children[index];
                if (child != null)
                {
                    double x = GetItemOffset(child);
// ReSharper disable CompareOfFloatsByEqualityOperator
                    if (x != 0)
// ReSharper restore CompareOfFloatsByEqualityOperator
                    {
                        Debug.WriteLine(child + " :" + x);
                    }

                    Rect r = Orientation == Orientation.Horizontal
                                 ? new Rect(offset + x, 0, child.DesiredSize.Width, finalSize.Height)
                                 : new Rect(0, offset + x, finalSize.Width, child.DesiredSize.Height);

                    child.Arrange(r);
                    offset += GetSize(child.DesiredSize);
                }
            }

            return finalSize;
        }

        #endregion

        #region Tab Swping

        // Using a DependencyProperty as the backing store for ItemOffsetProc.  This enables animation, styling, binding, etc...
        // NOTE: Don't remove it
        private static readonly DependencyProperty ItemOffsetProcProperty =
            DependencyProperty.Register("ItemOffsetProc", typeof (double), typeof (SwipePanel),
                                        new FrameworkPropertyMetadata(0.0,
                                                                      FrameworkPropertyMetadataOptions.AffectsArrange));

        private static readonly DependencyProperty ItemOffsetAnimationProperty =
            DependencyProperty.Register("ItemOffsetAnimation", typeof (double), typeof (SwipePanel),
                                        new FrameworkPropertyMetadata(0.0,
                                                                      FrameworkPropertyMetadataOptions.AffectsArrange));

        private static readonly DependencyProperty ItemOffsetProperty =
            DependencyProperty.RegisterAttached("ItemOffset", typeof (double), typeof (SwipePanel),
                                                new FrameworkPropertyMetadata(0.0));

        private readonly List<int> _animated = new List<int>();
        private int _lastSwipedIndex;
        private int _movedIndex;
        private Point? _point;

        private static double GetItemOffset(DependencyObject obj)
        {
            return (double) obj.GetValue(ItemOffsetProperty);
        }

        private static void SetItemOffset(DependencyObject obj, double value)
        {
            obj.SetValue(ItemOffsetProperty, value);
        }

        // Using a DependencyProperty as the backing store for ItemOffset.  This enables animation, styling, binding, etc...

        protected override void OnMouseMove(MouseEventArgs e)
        {
            if (!_point.HasValue || _movedIndex < 0)
            {
                if ((!UseSwipePiont || GetIsSwipePoint((DependencyObject) e.OriginalSource)) &&
                    e.LeftButton == MouseButtonState.Pressed)
                {
                    DependencyObject p = VisualTreeHelper.GetParent(this);
                    for (; p != null; p = VisualTreeHelper.GetParent(p))
                    {
                        if (GetIsSwipeLocked(p))
                        {
                            break;
                        }
                    }

                    if (p == null)
                    {
                        _point = e.GetPosition(this);
                        _movedIndex = GetChildIndexUnderMouse(Orientation, e.GetPosition, Children);
                        _lastSwipedIndex = _movedIndex;
                    }
                }
            }
            else
            {
                var moved = Children[_movedIndex] as FrameworkElement;

                if (moved != null)
                {
                    double pX;
                    double x;

                    if (Orientation == Orientation.Horizontal)
                    {
                        pX = e.GetPosition(this).X;
                        x = pX - _point.Value.X;
                    }
                    else
                    {
                        pX = e.GetPosition(this).Y;
                        x = pX - _point.Value.Y;
                    }

                    int n = GetChildIndexUnderMouse(Orientation, e.GetPosition, Children, _movedIndex);

                    if (n >= 0 && !_animated.Contains(n))
                    {
                        int d = Math.Sign(_movedIndex - n);

                        double dx = GetSize(moved.RenderSize);

                        var swiped = (FrameworkElement) Children[n];

                        double from = GetItemOffset(swiped);
                        // ReSharper disable CompareOfFloatsByEqualityOperator
                        double to = from != 0 ? 0 : d*dx;
                        // ReSharper restore CompareOfFloatsByEqualityOperator

                        _animated.Add(n);

                        _lastSwipedIndex = n;

                        var a = new DoubleAnimation
                                    {
                                        From = from,
                                        To = to,
                                        Duration = TimeSpan.FromMilliseconds(250),
                                        EasingFunction = new BackEase {EasingMode = EasingMode.EaseOut}
                                    };
                        a.Completed += delegate { _animated.Remove(n); };

                        swiped.BeginAnimation(ItemOffsetProperty, a);
                        BeginAnimation(ItemOffsetAnimationProperty, a);
                    }

                    SetItemOffset(moved, x);
                    SetValue(ItemOffsetProcProperty, x);
                }

            }
        }

        /// <summary>
        /// This static method gets index of item, that placed under the mouse
        /// </summary>
        /// <param name="orientation">Often it impossible to get access to an object of the SwipePanel. But the orientation is known.</param>
        /// <param name="getPosition">For example, the method GetPosition of MouseEventArgs</param>
        /// <param name="items">List of items</param>
        /// <param name="match">Index of item that not iterested for finding</param>
        /// <returns></returns>
        public static int GetChildIndexUnderMouse(Orientation orientation, Func<IInputElement, Point> getPosition, IList items, int match = -1)
        {
            int i = 0;
            for (; i < items.Count; i++)
            {
                var child = (UIElement) items[i];

                if (orientation == Orientation.Horizontal)
                {
                    double p = getPosition(child).X;

                    if (p >= 0 && p <= child.RenderSize.Width && match != i)
                    {
                        break;
                    }
                }
                else
                {
                    double p = getPosition(child).Y;

                    if (p >= 0 && p <= child.RenderSize.Height && match != i)
                    {
                        break;
                    }
                }
            }
            return i < items.Count ? i : -1;
        }

        protected override void OnMouseUp(MouseButtonEventArgs e)
        {
            if (_point.HasValue)
            {
                StopSwipe();
                if (_movedIndex != _lastSwipedIndex)
                {
                    var args = new MovingEventArgs(MovingEvent, this, _movedIndex, _lastSwipedIndex);
                    RaiseEvent(args);
                }
            }
            base.OnMouseDown(e);
        }

        protected override void OnMouseLeave(MouseEventArgs e)
        {
            if (_point.HasValue)
            {
                StopSwipe();
                if (_movedIndex >= 0)
                {
                    var args = new MovingEventArgs(DragingEvent, this, _movedIndex, -1);
                    RaiseEvent(args);
                    if (!args.Handled)
                    {
                        args = new MovingEventArgs(MovingEvent, this, _movedIndex, _lastSwipedIndex);
                        RaiseEvent(args);
                    }
                }
            }
            base.OnMouseLeave(e);
        }

        private void StopSwipe()
        {
            _point = null;
            _animated.Clear();
            foreach (FrameworkElement child in Children)
            {
                child.BeginAnimation(ItemOffsetProperty, null);
                SetItemOffset(child, 0.0);
            }
            SetValue(ItemOffsetProcProperty, 0.0);
        }

        #endregion
    }

    #region MovingEvent

    public delegate void MovingEventHandler(object sender, MovingEventArgs e);

    public class MovingEventArgs : RoutedEventArgs
    {
        public MovingEventArgs(RoutedEvent routedEvent, int oldIndex, int newIndex)
            : base(routedEvent)
        {
            OldIndex = oldIndex;
            NewIndex = newIndex;
        }

        public MovingEventArgs(RoutedEvent routedEvent, object source, int oldIndex, int newIndex)
            : base(routedEvent, source)
        {
            OldIndex = oldIndex;
            NewIndex = newIndex;
        }

        public int OldIndex { get; private set; }
        public int NewIndex { get; private set; }
    }

    #endregion
}